package visitors;

import nodes.*;
import nodes.body.*;
import nodes.declarations.*;
import nodes.exprnodes.*;
import nodes.exprnodes.bin.*;
import nodes.exprnodes.consts.*;
import nodes.exprnodes.sequences.*;
import nodes.exprnodes.unary.*;
import nodes.exprnodes.variables.*;
import nodes.statements.*;
import nodes.statements.controlflow.*;
import table.*;
import java.util.ArrayList;

public class SymbolTableVisitor implements Visitor
{
    private SymbolTable table;

    public SymbolTableVisitor()
    {     
    }

    public SymbolTable getTable()
    {
        return table;
    }

    /**
     * Visits ProgramNode.
     * Creates a scope for the declarations on this ProgramNode.
     * Creates a scope for the main program on body.
     *
     * @param n
     * @return
     */
    public Object visit(ProgramNode n)
    {
        table = new SymbolTable(n);
        if (n.decl!=null)
        {
            n.decl.accept(this);
        }
        table = table.beginScope(n.body);
        n.body.accept(this);
        table = table.endScope();
        return table;
    }

    public Object visit(BasicDeclarationNode n)
    {
        if (n.init != null)
            n.init.accept(this);
        VariableDeclaration varDec = new VariableDeclaration(n.type);
        if (!table.insert(n.id, varDec))
            ErrorHandler.complain("Error in BasicVariableDeclaration: Multiple instances of " + n.id);
        return null;
    }

    public Object visit(UserDeclarationNode n)
    {
        for (ExprNode cur = n.init; cur!=null; cur = cur.next)
        {
            cur.accept(this);
        }
        VariableDeclaration varDec = new VariableDeclaration(n.type);
        if (!table.insert(n.id, varDec))
            ErrorHandler.complain("Error in UserVariableDeclaration: Multiple instances of " + n.id);
        return null;
    }

    /**
     * Visits DataDeclarationNode.
     * Creates a scope on this DataDeclarationNode and stores reference
     * to this scope in DataDeclaration.
     *
     * @param n
     * @return
     */
    public Object visit(DataDeclarationNode n)
    {
        table = table.beginScope(n);
        ArrayList<Type> fields = new ArrayList<Type>();
        for (FieldNode cur = n.fields; cur!=null; cur = cur.next)
        {
            fields.add(cur.type);
            if (!table.insert(cur.id, new VariableDeclaration(cur.type)))
                    ErrorHandler.complain("Error in DataDeclaration: Multiple instances of " + cur.id);
        }
        DataDeclaration dataDec = new DataDeclaration(fields, table);
        table = table.endScope();
        if (!table.insert(n.name, dataDec)) ErrorHandler.complain("Error in DataDeclaration: Multiple instances of " + n.name);
        return null;
    }

    /**
     * Visits FunctionDeclarationNode.
     * Creates a scope on this FunctionDeclarationNode.
     *
     * @param n
     * @return
     */
    public Object visit(FunctionDeclarationNode n)
    {
        ArrayList<Type> para = new ArrayList<Type>();
        table = table.beginScope(n);
        if (n.parameters != null)
        {
            for (FieldNode cur = n.parameters; cur!=null; cur = cur.next)
            {
                para.add(cur.type);
                if (!table.insert(cur.id, new VariableDeclaration(cur.type)))
                    ErrorHandler.complain("Error in FunctionDeclaration: Multiple instances of " + cur.id);
            }
        }
        n.body.accept(this);
        FunctionDeclaration funcDec = new FunctionDeclaration(para, n.return_type);
        table = table.endScope();
        if (!table.insert(n.id, funcDec))
            ErrorHandler.complain("Error in FunctionDeclaration: Multiple instances of " + n.id);
        return null;
    }

    public Object visit(FunctionBodyNode n)
    {
        if (n.decl != null)
        {
            n.decl.accept(this);
        }
        if (n.stm != null)
        {
            n.stm.accept(this);
        }
        return null;
    }

    /**
     * Visits IfNode.
     * Creates an if scope on the body of if statement.
     * Creates an else scope on the body of else statement
     *
     * @param n
     * @return
     */
    public Object visit(IfNode n)
    {
        n.expr.accept(this);
        table = table.beginScope(n.body);
        n.body.accept(this);
        table = table.endScope();
        table = table.beginScope(n.elseBody);
        n.elseBody.accept(this);
        table = table.endScope();
        return null;
    }

    /**
     * Visits RepeatNode.
     * Creates a scope on the body.
     *
     * @param n
     * @return
     */
    public Object visit(RepeatNode n)
    {
        n.expr.accept(this);
        table = table.beginScope(n.body);
        n.body.accept(this);
        table = table.endScope();
        return null;
    }

    /**
     * Visits WhileNode.
     * Creates a scope on the body.
     *
     * @param n
     * @return
     */
    public Object visit(WhileNode n)
    {
        n.expr.accept(this);
        table = table.beginScope(n.body);
        n.body.accept(this);
        table = table.endScope();
        return null;
    }

    public Object visit(StatementListNode n)
    {
        n.left.accept(this);
        if (n.right != null)
        {
            n.right.accept(this);
        }
        return null;
    }

    public Object visit(BlockBodyNode n)
    {
        if (n.decl != null)
        {
            n.decl.accept(this);
        }
        if (n.stm != null)
        {
            n.stm.accept(this);
        }
        return null;
    }

    public Object visit(DeclarationListNode n)
    {
        if (n.right != null)
        {
            n.right.accept(this);
        }
        if (n.left != null)
        {
            n.left.accept(this);
        }
        return null;
    }

    public Object visit(BoolNode n)
    {
        return null;
    }

    public Object visit(AndNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(CharNode n)
    {
        return null;
    }

    public Object visit(ConcatNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(DivideNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(FloatNode n)
    {
        return null;
    }

    public Object visit(nodes.exprnodes.FuncCallNode n)
    {
        for (ExprNode cur = n.params; cur!=null; cur = cur.next)
        {
            cur.accept(this);
        }
        return null;
    }

    public Object visit(GreaterNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(GreaterOrEqualNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(IdNode n)
    {
        VariableDeclaration decl = table.lookupVariableDeclaration(n.id);
        if (decl == null)
            ErrorHandler.complain("Variable '" + n.id + "' was not declared: " + table.node.getClass().getSimpleName());
        return null;
    }

    public Object visit(InNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(IndexNode n)
    {
        VariableDeclaration decl = table.lookupVariableDeclaration(n.id);
        if (decl == null)
            ErrorHandler.complain("Variable '" + n.id + "' was not declared: " + table.node.getClass().getSimpleName());
        n.expr.accept(this);
        return null;
    }

    public Object visit(IntNode n)
    {
        return null;
    }

    public Object visit(LenNode n)
    {
        n.expr.accept(this);
        return null;
    }

    public Object visit(LessNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(LessOrEqualNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(ListNode n)
    {
        for (ExprNode cur = n.list; cur!=null; cur = cur.next)
        {
            cur.accept(this);
        }
        return null;
    }

    public Object visit(MinusNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(NotEqualNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(NotInNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(NotNode n)
    {
        n.expr.accept(this);
        return null;
    }

    public Object visit(OrNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(PlusNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(PowerNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(StringNode n)
    {
        return null;
    }

    public Object visit(TimesNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(TupleNode n)
    {
        for (ExprNode cur = n.list; cur!=null; cur = cur.next)
        {
            cur.accept(this);
        }
        return null;
    }

    public Object visit(EqualNode n)
    {
        n.left.accept(this);
        n.right.accept(this);
        return null;
    }

    public Object visit(nodes.statements.FuncCallNode n)
    {
        for (ExprNode cur = n.params; cur!=null; cur = cur.next)
        {
            cur.accept(this);
        }
        return null;
    }

    public Object visit(AssignNode n)
    {
        if (n.var!=null)
        {
            n.var.accept(this);
        }
        if (n.expr!=null)
        {
            n.expr.accept(this);
        }
        return null;
    }

    /**
     * Visits FieldNode.
     * This should never get called because, to provide meaningful messages,
     * we iterate throught them, not recurse.
     *
     * @param n
     * @return
     */
    public Type visit(FieldNode n)
    {
        throw new UnsupportedOperationException("Fields should be iterated through!");
    }

    /**
     * Visits VariableListNode.
     * This should never get called because, for efficiency,
     * we iterate through the list, not recurse.
     *
     * @param n
     * @return
     */
    public Object visit(VariableListNode n)
    {
        n.left.accept(this);
        return null;
    }
}